package com.example.seckill.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 扣库存
 *
 * @author 丁昌江
 */
@Slf4j
@Service
public class StockService {

    /**
     * 库存不足
     */
    public static final long STOCK_OUTS = -2L;
    /**
     * 库存未初始化
     */
    public static final long UNINITIALIZED_STOCK = -3L;
    /**
     * 商品不存在
     */
    public static final long GOODS_UNEXISTS = -4L;
    /**
     * 商品已购买
     */
    public static final long GOODS_PURCHASED = -5L;
    /**
     * 没抢到锁
     */
    public static final long DO_NOT_GRAB_LOCK = -6L;


    /**
     * Redis 客户端
     */
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private RedissonClient redissonClient;

    /**
     * 执行扣库存的脚本
     */
    public static final String STOCK_LUA;

    static {
        /*
         * 扣减库存Lua脚本
         * KEYS[1] 就是 商品 key
         * ARGV[1] 就是 扣减的数量 num
         *
         * KEYS[2] 就是 已购商品列表 key
         * ARGV[2] 就是 当前用户id userId
         *
         * 返回：
         *   -5：已购买过
         *   -3：库存未初始化
         *   -2：库存不足
         *   -1：不限库存
         *   >=0：剩余库存（扣减之后剩余的库存）
         */


        // 命令：exists key 存在返回1 不存在返回0
        STOCK_LUA =
                "if (redis.call('exists', KEYS[1]) == 1) then" +
                // 命令：get key 存在返回value，不存在返回null
                // tonumber()，lua函数，将指定类型转成数字类型
                "    local stock = tonumber(redis.call('get', KEYS[1]));" +
                "    local num = tonumber(ARGV[1]);" +
                // 如果get key 返回 -1 表示不限库存
                "    if (stock == -1) then" +
                "        return -1;" +
                "    end;" +
                // 如果库存 > 扣减数量，再判断是否已经购买过，购买过返回-5；没购买过返回扣减后的库存数量
                "    if (stock >= num) then" +
                "        if(redis.call('sadd', KEYS[2],ARGV[2]) == 0) then" +
                "            return -5;" +
                "        end;" +
                "        return redis.call('incrby', KEYS[1], 0 - num);" +
                "    end;" +
                // 反之就是库存不足
                "    return -2;" +
                "end;" +
                // 如果key不存在，说明没有进行过初始化
                "return -3;";
    }

    /**
     * 扣减库存方法：
     * 1 如果没有初始化库存会进行初始化
     * 2 扣减库存（判断是否存在购买记录->扣减库存）
     *
     * @param key               库存key
     * @param expire            库存有效时间,单位秒
     * @param initStockSupplier 初始化库存回调函数
     * @return -6：未抢到锁 -5：已经购买过 -4: 商品不存在 -3：未初始化 -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存
     */
    public Long stock(String key, String purchasedKey, String userId, Long expire, Supplier<Long> initStockSupplier) {

        int num = 1;

        // 1 先扣减库存
        long stock = stock(key, num, purchasedKey, userId);

        // 2 如果库存未初始化则初始化库存
        if (stock == UNINITIALIZED_STOCK) { // 这一行，还是会有多个线程进入
            RLock lock = redissonClient.getLock("lock:" + key);
            try {
                // 尝试2s内获取锁，获取失败进入false
                if (lock.tryLock(200, TimeUnit.MILLISECONDS)) {
                    // 双重验证，避免并发时重复回源到数据库
                    stock = stock(key, num, purchasedKey, userId);
                    if (stock == UNINITIALIZED_STOCK) {
                        // 获取初始化库存
                        final Long initStock = initStockSupplier.get();
                        if (Objects.isNull(initStock)) return GOODS_UNEXISTS;
                        // 将库存设置到redis
                        redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
                        // 调一次扣库存的操作
                        stock = stock(key, num, purchasedKey, userId);
                        // 设置已购集合过期时间
                        redisTemplate.expire(purchasedKey, expire, TimeUnit.SECONDS);
                    }
                } else {
                    return DO_NOT_GRAB_LOCK;
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                if (lock.isLocked()) {
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                    }
                }
            }
        }
        return stock;
    }


    /**
     * 扣库存
     *
     * @param key 库存key
     * @param num 扣减库存数量
     * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】
     */
    private Long stock(String key, Integer num,String purchasedKey,String userId) {
        // 脚本里的KEYS参数
        List<String> keys = new ArrayList<>();
        keys.add(key);
        keys.add(purchasedKey);
        // 脚本里的ARGV参数
        // List<String> args = new ArrayList<>();
        // args.add(num.toString());

        // 指定 lua 脚本，并且指定返回值类型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(STOCK_LUA, Long.class);
        return redisTemplate.execute(redisScript, keys, num,userId);
    }

    // -------------- 暂时用不到 ---------------

    /**
     * 加库存 缓存库存
     *
     * @param key    库存key
     * @param expire 过期时间（秒）
     * @param num    库存数量
     * @return 最新库存数量
     */
    public Long addStock(String key, Long expire, Long num) {
        Boolean hasKey = redisTemplate.hasKey(key);
        // 判断key是否存在，存在就直接更新
        if (Boolean.TRUE.equals(hasKey)) {
            return redisTemplate.opsForValue().increment(key, num);
        }

        Assert.notNull(expire, "初始化库存失败，库存过期时间不能为null");
        RLock lock = redissonClient.getLock("lock:"+key);
        try {
            if (lock.tryLock(200,TimeUnit.MILLISECONDS)) {
                // 获取到锁后再次判断一下是否有key
                hasKey = redisTemplate.hasKey(key);
                if (Boolean.FALSE.equals(hasKey)) {
                    // 初始化库存
                    redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS);
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            if(lock.isLocked()){
                if(lock.isHeldByCurrentThread()){
                    lock.unlock();
                }
            }
        }

        return num;
    }

    /**
     * 获取库存
     *
     * @param key 库存key
     * @return -1:不限库存; 大于等于0:剩余库存
     */
    public Long getStock(String key) {
        Long stock = (Long) redisTemplate.opsForValue().get(key);
        return stock == null ? -1L : stock;
    }


}
